package com.zh.common.security;

import com.zh.common.entity.Oauth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor;
import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedAuthoritiesExtractor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;

import java.util.*;

/**
 * code is far away from bug with the animal protecting
 * ┏┓　　　┏┓
 * ┏┛┻━━━┛┻┓
 * ┃　　　　　　　┃
 * ┃　　　━　　　┃
 * ┃　┳┛　┗┳　┃
 * ┃　　　　　　　┃
 * ┃　　　┻　　　┃
 * ┃　　　　　　　┃
 * ┗━┓　　　┏━┛
 * 　　┃　　　┃神兽保佑
 * 　　┃　　　┃代码无BUG！
 * 　　┃　　　┗━━━┓
 * 　　┃　　　　　　　┣┓
 * 　　┃　　　　　　　┏┛
 * 　　┗┓┓┏━┳┓┏┛
 * 　　　┃┫┫　┃┫┫
 * 　　　┗┻┛　┗┻┛
 *
 * @Description: 配置资源服务器获取认证主体
 * @Author: Li.HuiNeng
 * @Create in: 2019-10-11 15:38
 * @Version: 1.0.1
 */
public class CustomUserInfoTokenServices implements ResourceServerTokenServices {
    private static final Logger logger = LoggerFactory.getLogger(CustomUserInfoTokenServices.class);

    private static final String[] PRINCIPAL_KEYS = new String[]{"principal", "id" , "typeId" , "userRoleId", "userTrueName" , "userLoginName", "userPassword" };

    private final String userInfoEndpointUrl;

    private final String clientId;

    private final String secret;

    private OAuth2RestOperations restTemplate;

    private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE;

    private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();

    public CustomUserInfoTokenServices(String userInfoEndpointUrl, String clientId, String secret) {
        this.userInfoEndpointUrl = userInfoEndpointUrl;
        this.clientId = clientId;
        this.secret = secret;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }

    public void setRestTemplate(OAuth2RestOperations restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) {
        this.authoritiesExtractor = authoritiesExtractor;
    }

    @Override
    public OAuth2Authentication loadAuthentication(String accessToken)
            throws AuthenticationException, InvalidTokenException {
        Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
        if (map.containsKey("error" )) {
            logger.warn("userinfo returned error: " + map.get("error" ));
            throw new InvalidTokenException(accessToken);
        }
        return extractAuthentication(map);
    }



    private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
        Object principal = null;
        try {
            principal = map;
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (!(map.get("name").equals("fuhuiba") || map.get("name").equals("captcha"))) {
            principal = getPrincipal(map);
        }

        OAuth2Request request = getRequest(map);
        List<GrantedAuthority> authorities = this.authoritiesExtractor
                .extractAuthorities(map);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( principal, "N/A" , authorities);
        token.setDetails(map);
        return new OAuth2Authentication(request, token);
    }

    @SuppressWarnings({"unchecked"})
    private Object getPrincipal(Map<String, Object> map) {
        Oauth auth = new Oauth();
        for (String key : PRINCIPAL_KEYS) {
            if (map.containsKey(key)) {
                Map<String, Object> resMap = (Map<String, Object>) map.get(key);
                if (resMap.get("id" ) != null) {
                    auth.setId(Long.parseLong(resMap.getOrDefault("id" , 0).toString()));
                }
                if (resMap.get("typeId" ) != null) {
                    auth.setTypeId(Integer.valueOf(resMap.getOrDefault("typeId" , "unknown" ).toString()));
                }
                if (resMap.get("userRoleId" ) != null) {
                    auth.setUserRoleId(resMap.getOrDefault("userRoleId" , "unknown" ).toString());
                }
                if (resMap.get("userTrueName" ) != null) {
                    auth.setUserTrueName(resMap.getOrDefault("userTrueName" , "unknown" ).toString());
                }
                if (resMap.get("userLoginName" ) != null) {
                    auth.setUserLoginName(resMap.getOrDefault("userLoginName" , "unknown" ).toString());
                }
                if (resMap.get("userPassword" ) != null) {
                    auth.setUserPassword(resMap.getOrDefault("userPassword" , 0).toString());
                }
                return auth;
            }
        }
        return "unknown" ;
    }

    @SuppressWarnings({"unchecked"})
    private OAuth2Request getRequest(Map<String, Object> map) {
        Map<String, Object> request = (Map<String, Object>) map.get("oauth2Request" );

        String clientId = (String) request.get("clientId" );
        Set<String> scope = new LinkedHashSet<>(request.containsKey("scope" ) ?
                (Collection<String>) request.get("scope" ) : Collections.emptySet());

        return new OAuth2Request(null, clientId, null, true, new HashSet<>(scope),
                null, null, null, null);
    }

    @Override
    public OAuth2AccessToken readAccessToken(String accessToken) {
        throw new UnsupportedOperationException("Not supported: read access token" );
    }

    @SuppressWarnings({"unchecked"})
    private Map<String, Object> getMap(String path, String accessToken) {
        logger.debug("Getting user info from: " + path);
        try {
            OAuth2RestOperations restTemplate = this.restTemplate;
            if (restTemplate == null) {
                BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
                resource.setClientId(this.clientId);
                resource.setClientSecret(this.secret);
                restTemplate = new OAuth2RestTemplate(resource);
            }
            OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext().getAccessToken();
            if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
                DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
                        accessToken);
                token.setTokenType(this.tokenType);
                restTemplate.getOAuth2ClientContext().setAccessToken(token);
            }
            return restTemplate.getForEntity(path, Map.class).getBody();
        } catch (Exception ex) {
            logger.warn("Could not fetch user details: " + ex.getClass() + ", "
                    + ex.getMessage());
            return Collections.singletonMap("error" ,
                    "Could not fetch user details" );
        }
    }
}
